/*
 *  Created at 2011-12-06 21:50 GMT-03:00
 *  Author: Victor Williams Stafusa da Silva
 *  JSmartUnit
 */
package org.jsmartunit.invocations;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.jsmartunit.annotations.RunMethodWith;
import org.jsmartunit.model.InstanceFactory;
import org.jsmartunit.model.MethodRunnerFactory;
import org.jsmartunit.model.TestRunner;
import org.jsmartunit.model.TestRunnerFactory;
import org.jsmartunit.statement.NotifyIgnoreRunner;
import org.jsmartunit.statement.ThrowsException;
import org.jsmartunit.util.Stringfier;

import org.junit.Ignore;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

/**
 * @author Victor Williams Stafusa da Silva
 */
public class MethodInvocationArgumentBuilder implements TestRunnerFactory {

    private final RunNotifier notifier;
    private final InstanceFactory<?> instanceFactory;
    private final Method method;
    private final String prefix;

    public MethodInvocationArgumentBuilder(String prefix, RunNotifier notifier, InstanceFactory<?> instanceFactory, Method method) {
        if (prefix == null) throw new IllegalArgumentException();
        if (notifier == null) throw new IllegalArgumentException();
        if (instanceFactory == null) throw new IllegalArgumentException();
        if (method == null) throw new IllegalArgumentException();

        this.prefix = prefix;
        this.notifier = notifier;
        this.instanceFactory = instanceFactory;
        this.method = method;
    }

    public InstanceFactory<?> getInstanceFactory() {
        return instanceFactory;
    }

    public Method getMethod() {
        return method;
    }

    public Annotation[] getAnnotations() {
        return method.getAnnotations();
    }

    @Override
    public <E extends Annotation> E getAnnotation(Class<E> annotationClass) {
        return method.getAnnotation(annotationClass);
    }

    @Override
    public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return method.isAnnotationPresent(annotationClass);
    }

    public Class<?>[] getParameterTypes() {
        return method.getParameterTypes();
    }

    private TestRunner createRunner(String name, Object... args) throws InitializationError {
        return new MethodInvocation(name, notifier, getInstanceFactory(), getMethod(), args).createRunner();
    }

    @Override
    public TestRunner createNoArgRunner() throws InitializationError {
        return createRunner(prefix + method.getName());
    }

    @Override
    public <T> List<TestRunner> createSingleArgRunners(List<T> params, Stringfier<T> s) throws InitializationError {
        List<TestRunner> runners = new ArrayList<TestRunner>(params.size());

        for (T elem : params) {
            runners.add(createRunner(prefix + method.getName() + "[" + (s == null ? elem.toString() : s.toString(elem)) + "]", elem));
        }

        return runners;
    }

    @Override
    public List<TestRunner> createMultiArgRunners(List<Object[]> params, Stringfier<Object[]> s) throws InitializationError {
        List<TestRunner> runners = new ArrayList<TestRunner>(params.size());

        for (Object[] elem : params) {
            String childName = prefix + method.getName() + (s == null ? Arrays.asList(elem).toString() : s.toString(elem));
            runners.add(createRunner(childName, notifier, elem));
        }

        return runners;
    }

    public List<TestRunner> runnersFor() {
        if (isAnnotationPresent(Ignore.class)) {
            TestRunner methodRunner = new TestRunner(method.getName(), method.getAnnotations());
            Statement rec = NotifyIgnoreRunner.ignorerFor(methodRunner, notifier);
            methodRunner.setStatement(rec);
            return Collections.singletonList(methodRunner);
        }

        try {
            RunMethodWith rmw = getAnnotation(RunMethodWith.class);
            if (rmw == null) {
                for (Annotation a : getAnnotations()) {
                    RunMethodWith rmw2 = a.annotationType().getAnnotation(RunMethodWith.class);
                    if (rmw2 != null) break;
                    if (rmw != null) throw new InitializationError("More than one @RunMethodWith annotation found for test method " + getMethod() + ".");
                    rmw = rmw2;
                }
            }
            Class<?>[] paramsTypes = getParameterTypes();

            if (rmw != null) {
                Class<? extends MethodRunnerFactory> factoryClass = rmw.value();
                MethodRunnerFactory factory;
                try {
                    Constructor<? extends MethodRunnerFactory> ctor = factoryClass.getDeclaredConstructor();
                    ctor.setAccessible(true);
                    factory = ctor.newInstance();
                } catch (InvocationTargetException e) {
                    throw new InitializationError(Collections.singletonList(e.getCause()));
                } catch (Throwable e) {
                    throw new InitializationError(Collections.singletonList(e));
                }
                return factory.create(this);
            }

            if (paramsTypes.length == 0) return Collections.singletonList(createNoArgRunner());
            throw new InitializationError("No runner known for test method " + getMethod() + ".");
        } catch (InitializationError e) {
            TestRunner runner = new TestRunner(method.getName(), method.getAnnotations());
            runner.setStatement(ThrowsException.throwing(e));
            return Collections.singletonList(runner);
        }
    }
}
